using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using DecisionMaking.BusinessLogic.Model;
using LumenWorks.Framework.IO.Csv;
using NHibernate.Exceptions;
using NHibernate;
using DecisionMaking.Domain;
using Castle.Services.Transaction;
using Environment = DecisionMaking.Domain.Environment;

namespace DecisionMaking.BusinessLogic.Impl
{
	[Transactional]
	public class EnvironmentManager : IEnvironmentManager
	{
        public IAccountManager AccountManager { get; set; } // injected by Windsor
		
		#region Environments

        [Transaction(TransactionMode.Supported)]
        public Environment GetEnvironment(int environmentId)
		{
			try
			{
				return Environment.Find(environmentId);
			}
			catch (ObjectNotFoundException e)
			{
				throw new EntityNotFoundException(typeof(Environment), environmentId, e);
			}
		}
		
		[Transaction(TransactionMode.Supported)]
		public IList<Environment> GetEnvironments (int accountId)
		{
			return Environment.FindByAccountId(accountId);
		}

        [Transaction(TransactionMode.Supported)]
        public IList<Environment> GetPublicAndUsersEnvironments(int accountId)
	    {
            return Environment.FindByAccountIdOrPublic(accountId);
	    }

	    [Transaction(TransactionMode.Requires)]
		public Environment SaveEnvironment (EnvironmentModel model)
		{
			Environment environment;
			
			if (model.Id == null)
			{
				// new environment, assign account from model
				environment = new Environment
				{
				    Account = AccountManager.GetAccount(model.AccountId.Value)
				};
			}
			else
			{
				// existing environment, load and make sure account from model is the owner
				environment = GetEnvironment(model.Id.Value);
				if (environment.Account.Id != model.AccountId)
				{
					throw new AccessDeniedException(model.AccountId.Value, typeof(Environment), model.Id.Value);
				}				
			}
			
		    try 
		    {
				environment.Name = model.Name;
				environment.Description = model.Description;
				environment.IndependentVariableName = model.IndependentVariable;
				environment.Settings.IsPublic = model.IsPublic;
				
				environment.Save();

                // when creating a new environment, add 3 default cue attributes
                if (model.Id == null)
                {                    
                    CreateDefaultCueAttributes(environment);       
                }
				
				return environment;
			}
			catch (ConstraintViolationException e) 
			{
				if (e.ConstraintName == "uix_environment_name") 
				{
					throw new DuplicateEnvironmentNameException(model.Name, e);
				}
				else 
				{
					throw; // unexpected exception, throw to caller
				}
			}
					
		}

        /// <summary>
        /// Checks if the specified environment can be set to active and if so, sets it to active and saves it.
        /// </summary>
        /// <param name="environment"></param>
        private void MakeActiveIfPossible(Environment environment)
        {
            if (environment.HasCueAttributes() && environment.HasCues() && environment.HasCandidates() && environment.HasAgents())
            {
                environment.Status = Status.ACTIVE;
                environment.Save();
            }
        }

		#endregion Environments
		
		#region CueAttributes

        [Transaction(TransactionMode.Supported)]
		public IList<CueAttribute> GetCueAttributes (int environmentId)
		{
			return CueAttribute.FindByEnvironmentId(environmentId);
		}

        [Transaction(TransactionMode.Supported)]
        public CueAttribute GetCueAttribute(int id)
	    {
	        return CueAttribute.Find(id);
	    }

	    [Transaction(TransactionMode.Requires)]
		public CueAttribute AddCueAttribute (CueAttributeModel model)
		{
			try
			{
				var attribute = new CueAttribute
				{
					Environment = GetEnvironment(model.EnvironmentId),
					Name = model.Name
				};
				
				attribute.Save();

			    MakeActiveIfPossible(attribute.Environment);
				
				return attribute;
			}
			catch (ConstraintViolationException e) 
			{
				if (e.ConstraintName == "uix_cue_attribute_name") 
				{
					throw new DuplicateCueAttributeNameException(model.Name, e);
				}
				else 
				{
					throw; // unexpected exception, throw to caller
				}
			}			
			
		}

        [Transaction(TransactionMode.Requires)]
		public void RemoveCueAttribute (int id)
		{
			CueAttribute.Find(id).Delete();
		}

		#endregion CueAttributes
		
		#region Cues

        [Transaction(TransactionMode.Supported)]
        public IList<Cue> GetCues(int environmentId)
		{
			return Cue.FindByEnvironmentId(environmentId);
		}

         [Transaction(TransactionMode.Supported)]
	    public Cue GetCue(int id)
	    {
             return Cue.Find(id);
	    }

	    [Transaction(TransactionMode.Requires)]
		public Cue SaveCue (CueModel model)
		{
            Cue cue;

            if (model.CueId == null)
            {
                cue = new Cue
                {
                    Environment = GetEnvironment(model.EnvironmentId),
                };
            }
            else
            {
                cue = Cue.Find(model.CueId);
                if (cue.Environment.Id != model.EnvironmentId)
                {
                    // disallow changing a cue's environment
                    throw new AccessDeniedException(model.EnvironmentId, typeof(Candidate), model.CueId.Value);
                }

                // remove all existing attribute values
                cue.AttributeValues.Clear();
            }

            cue.Name = model.Name;
	        cue.SortOrder = model.Order;

            // save the cue before adding the new attribute values to avoid having to manually check which attribute values have changed
            try
            {
                cue.SaveAndFlush();

                MakeActiveIfPossible(cue.Environment);
            }
            catch (ConstraintViolationException e)
            {
                if (e.ConstraintName == "uix_cue_name")
                {
                    throw new DuplicateCueNameException(model.Name, e);
                }
                else
                {
                    throw;
                }
            }

            // add all attributes to the cue
            foreach (var valueModel in model.AttributeValues)
            {
                cue.AttributeValues.Add(
                    new CueAttributeValue
                    {
                        Cue = cue, 
                        CueAttribute = CueAttribute.Find(valueModel.CueAttributeId),
                        Value = valueModel.Value.GetValueOrDefault()
                    }
                );
            }

            cue.Save();

		    return cue;
		}

        [Transaction(TransactionMode.Requires)]
		public void RemoveCue (int id)
		{
			Cue.Find(id).Delete();
		}

		#endregion Cues

        #region Candidates

        [Transaction(TransactionMode.Supported)]
        public Candidate GetCandidate(int id)
	    {
            try
            {
                return Candidate.Find(id);
            }
            catch (ObjectNotFoundException e)
            {
                throw new EntityNotFoundException(typeof(Candidate), id, e);
            }
	    }

        [Transaction(TransactionMode.Supported)]
        public IList<Candidate> GetCandidates(int environmentId)
		{
            return Candidate.FindByEnvironmentId(environmentId);
        }

        [Transaction(TransactionMode.Requires)]
        public Candidate SaveCandidate(CandidateModel model)
        {
            Candidate candidate;
            if (model.CandidateId.HasValue)
            {
                candidate = Candidate.Find(model.CandidateId);
                if (candidate.Environment.Id != model.EnvironmentId)
                {
                    // disallow changing a candidate's environment
                    throw new AccessDeniedException(model.EnvironmentId.GetValueOrDefault(), typeof(Candidate), model.CandidateId.Value);
                }

                // delete all old cue values
                candidate.CueValues.Clear();
            }
            else
            {
                candidate = new Candidate
                {
                    Environment = GetEnvironment(model.EnvironmentId.GetValueOrDefault())
                };
            }

            candidate.Name = model.Name;
            candidate.IndependentVariableValue = model.IndependentVariableValue.GetValueOrDefault();

            // save the cue before adding the new attribute values to avoid having to manually check which attribute values have changed
            try
            {
                candidate.SaveAndFlush();

                MakeActiveIfPossible(candidate.Environment);
            }
            catch (ConstraintViolationException e)
            {
                if (e.ConstraintName == "uix_candidate_name")
                {
                    throw new DuplicateCandidateNameException(model.Name, e);
                }
                else
                {
                    throw; // unexpected exception, throw to caller
                }
            }

            // add all cue values to the candidate
            foreach (var valueModel in model.CueValues)
            {
                candidate.CueValues.Add(
                    new CandidateCueValue
                        {
                        Candidate = candidate,
                        Cue = Cue.Find(valueModel.CueId),
                        Value = valueModel.Value.GetValueOrDefault()
                    }
                );
            }

            candidate.Save();

            return candidate;

        }

        [Transaction(TransactionMode.Requires)]
        public void RemoveCandidate(int id)
        {
            Candidate.Find(id).Delete();
        }

        #endregion Candidates

        #region Agents

        [Transaction(TransactionMode.Supported)]
	    public IList<Agent> GetAgents(int environmentId)
	    {
            return Agent.FindByEnvironmentId(environmentId);
	    }

	    [Transaction(TransactionMode.Requires)]
	    public void GenerateAgents(int environmentId, int amount)
	    {
            var environment = GetEnvironment(environmentId);
            var candidates = GetCandidates(environmentId);
            var random = new Random();

            // delete all existing agents
            Agent.DeleteAllInEnvironment(environmentId);

            for (var i = 0; i < amount; i++)
            {
                var agent = new Agent {Environment = environment};
                var candidateAmount = random.Next(candidates.Count); // take a random number of candidates
                var candidateOffset = random.Next(candidates.Count - candidateAmount); // skip a random number of candidates out of those not taken
                agent.PctOfCuesKnown = GetRandomPctOfCuesKnown();

                // recognize a random number of candidates
                agent.CandidatesRecognized = candidates.Skip(candidateOffset).Take(candidateAmount).ToList();

                // select random cues from the list, the number of which to be determined by the agent's PctOfCuesKnown value
                foreach (var candidate in agent.CandidatesRecognized)
                {
                    var values = 0;
                    while (values < (int) Math.Ceiling(candidate.CueValues.Count * agent.PctOfCuesKnown))
                    {
                        var cueValue = candidate.CueValues[random.Next(0, candidate.CueValues.Count)]; // pick a random cue value;

                        if (!agent.CandidateCueValues.Contains(cueValue))
                        {
                            agent.CandidateCueValues.Add(cueValue);
                            values++;
                        }
                    }

                }

                agent.Save();
            }

            MakeActiveIfPossible(environment);
	    }

        /// <summary>
        /// Simulates the type casting that the original researchers used
        /// </summary>
        /// <returns></returns>
        private double GetRandomPctOfCuesKnown()
        {
            var random = new Random();
            var type = random.Next(0, 6);

            switch (type)
            {
                case 0:
                    return 0;
                case 1:
                    return 0.1;
                case 2:
                    return 0.2;
                case 3:
                    return 0.5;
                case 4:
                    return 0.75;
                case 5:
                    return 1.0;
            }
            
            return 0; // should never happen
        }

        #endregion Agents

        [Transaction(TransactionMode.Requires)]
	    public Environment ImportCsv(int accountId, Stream csvInput)
	    {
	        using (var reader = new CsvReader(new StreamReader(csvInput), true, ','))
	        {
                // we assume that the first column header contains environment name, second column header contains independent variable name and the rest contain cue names
	            var headers = reader.GetFieldHeaders();

                // create environment
	            var environment = new Environment
	            {
	                Account = AccountManager.GetAccount(accountId),
	                Name = headers[0],
	                IndependentVariableName = headers[1]
	            };

	            try
	            {
                    environment.Save();
	            }
			    catch (ConstraintViolationException e) 
			    {
				    if (e.ConstraintName == "uix_environment_name") 
				    {
					    throw new DuplicateEnvironmentNameException(headers[0], e);
				    }
				    else 
				    {
					    throw; // unexpected exception, throw to caller
				    }
			    }
                
                // create 3 default cue attributes
                CreateDefaultCueAttributes(environment);

                // create cues from the other headers

                // store created cues in an indexed array so that we can later create CandidateCueValues for them
                var cues = new Cue[headers.Length - 2];

                for (var i = 2; i < headers.Length; i++)
                {

                    try
                    {
                        var cue = new Cue
                        {
                            Environment = environment,
                            Name = headers[i],
                            SortOrder = i - 1 // set order cue attribute according to the natural order in the CSV file, 1-based
                        };

                        cue.Save();

                        cues[i - 2] = cue;
                    }
		            catch (ConstraintViolationException e)
		            {
		                if (e.ConstraintName == "uix_cue_name")
		                {
		                    throw new DuplicateCueNameException(headers[i], e);
		                }
		                else
		                {
		                    throw;
		                }
		            }

                }

                // create candidates from the rows
	            foreach (var row in reader)
	            {
	                Candidate candidate;
	                try
	                {
	                    candidate = new Candidate
	                    {
	                        Environment = environment,
	                        Name = row[0],
	                        IndependentVariableValue = Double.Parse(row[1])
	                    };
	                }
	                catch (FormatException e)
	                {
	                    throw new InvalidCueValueException((int)reader.CurrentRecordIndex, 1, e);
	                }

                    // create cue values
                    for (var i = 2; i < row.Length; i++)
                    {
                        try
                        {
                            candidate.CueValues.Add(new CandidateCueValue
                            {
                                Candidate = candidate,
                                Cue = cues[i - 2],
                                Value = Double.Parse(row[i])
                            });
                        }
                        catch (FormatException e)
                        {
                            throw new InvalidCueValueException((int)reader.CurrentRecordIndex, i, e);
                        }
                    }

                    candidate.Save();
	            }

	            return environment;
	        }
	    }

        private void CreateDefaultCueAttributes(Environment environment)
        {
            var validity = new CueAttribute
            {
                Environment = environment,
                Name = "Ecological Validity"
            };

            var discrimination = new CueAttribute
            {
                Environment = environment,
                Name = "Discrimination Rate"
            };

            validity.Save();
            discrimination.Save();
        }
	}
}

